﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Threading;
using System.Net.Sockets;

namespace SyncDBServer
{
    class SocketListener
    {
        ServiceSettings settings;
        BufferManager buf_manager;
        IPEndPoint listen_point;
        Socket listen_sock;

        SocketAsyncEventArgsPool pool_accept_args;
        SocketAsyncEventArgsPool pool_transf_args;

        Semaphore connection_enforcer;
        int accepted_num;

        public SocketListener()
        {
            this.settings = ServiceSettings.GetInstance();

            this.buf_manager = new BufferManager(this.settings.BufferSize * this.settings.MaxConnectionNum * 2, this.settings.BufferSize * this.settings.MaxConnectionNum);
            this.listen_point = Utils.MakeIPEndPoint(this.settings.ListenPoint);

            this.pool_accept_args = new SocketAsyncEventArgsPool(this.settings.MaxAcceptNum);
            this.pool_transf_args = new SocketAsyncEventArgsPool(this.settings.MaxConnectionNum);

            this.connection_enforcer = new Semaphore(this.settings.MaxConnectionNum, this.settings.MaxConnectionNum);
            this.accepted_num = 0;

            Init();
            StartListen();
        }

        private void Init()
        {
            for (int i = 0; i < this.settings.MaxAcceptNum; i++)
            {
                this.pool_accept_args.Push(CreateNewSaeaForAccept(pool_accept_args));
            }

            for (int i = 0; i < this.settings.MaxConnectionNum; i++)
            {
                SocketAsyncEventArgs saea = new SocketAsyncEventArgs();
                this.buf_manager.SetBuffer(saea);
                saea.Completed += new EventHandler<SocketAsyncEventArgs>(TransferCompleted);
            }
        }

        private void StartListen()
        {
            listen_sock = new Socket(this.listen_point.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            listen_sock.Bind(this.listen_point);
            listen_sock.Listen(this.settings.WaitAcceptNum);

            StartAccept();
        }

        private void AcceptCompleted(object sender, SocketAsyncEventArgs e)
        {
            ProcessAccept(e);
        }

        private void TransferCompleted(object sender, SocketAsyncEventArgs Saea)
        {
            switch (Saea.LastOperation)
            {
                case SocketAsyncOperation.Receive:
                    ProcessReceive(Saea);
                    break;

                case SocketAsyncOperation.Send:
                    ProcessSend(Saea);
                    break;

                default:
                    throw new ArgumentException("The last operation completed on the socket was not a receive or send");
            }
        }

        internal SocketAsyncEventArgs CreateNewSaeaForAccept(SocketAsyncEventArgsPool pool)
        {
            SocketAsyncEventArgs acceptEventArg = new SocketAsyncEventArgs();
            acceptEventArg.Completed += new EventHandler<SocketAsyncEventArgs>(AcceptCompleted);
            return acceptEventArg;
        }

        internal void StartAccept()
        {
            SocketAsyncEventArgs saea;

            if (this.pool_accept_args.Count > 1)
            {
                try
                {
                    saea = this.pool_accept_args.Pop();
                }
                catch
                {
                    saea = CreateNewSaeaForAccept(pool_accept_args);
                }
            }
            else
                saea = CreateNewSaeaForAccept(pool_accept_args);

            this.connection_enforcer.WaitOne();

            if (!listen_sock.AcceptAsync(saea))
                ProcessAccept(saea);
        }

        private void ProcessAccept(SocketAsyncEventArgs acceptEventArgs)
        {
            if (acceptEventArgs.SocketError != SocketError.Success)
            {
                StartAccept();
                HandleBadAccept(acceptEventArgs);
                return;
            }

            Interlocked.Increment(ref this.accepted_num);

            StartAccept();

            SocketAsyncEventArgs transf_saea = this.pool_transf_args.Pop();
            transf_saea.AcceptSocket = acceptEventArgs.AcceptSocket;
            acceptEventArgs.AcceptSocket = null;
            this.pool_accept_args.Push(acceptEventArgs);

            StartReceive(transf_saea);
        }

        private void HandleBadAccept(SocketAsyncEventArgs Saea)
        {
            //This method closes the socket and releases all resources, both
            //managed and unmanaged. It internally calls Dispose.           
            Saea.AcceptSocket.Close();

            //Put the SAEA back in the pool.
            pool_accept_args.Push(Saea);

            // a.mavrin: this is difference from example
            this.connection_enforcer.Release();
        }

        private void StartReceive(SocketAsyncEventArgs Saea)
        {
            DataHolder holder = Saea.UserToken as DataHolder;
            Saea.SetBuffer(holder.OffsetRecv, this.settings.BufferSize);

            bool willRaiseEvent = Saea.AcceptSocket.ReceiveAsync(Saea);
            if (!willRaiseEvent)
            {
                ProcessReceive(Saea);
            }
        }

        private void ProcessReceive(SocketAsyncEventArgs Saea)
        {
            DataHolder holder = (DataHolder)Saea.UserToken;
            if (Saea.SocketError != SocketError.Success)
            {
                holder.Reset();
                CloseClientSocket(Saea);
                return;
            }

            // If no data was received, close the connection. This is a NORMAL
            // situation that shows when the client has finished sending data.
            if (Saea.BytesTransferred == 0)
            {
                holder.Reset();
                CloseClientSocket(Saea);
                return;
            }

            byte[] pack = holder.ExtractReceivedPackage();
            if (pack == null)
            {
                StartReceive(Saea);
            }
            else
            {
                // TODO process receive data and pack answer into SAEA buffer
                StartSend(Saea);
            }
        }

        private void StartSend(SocketAsyncEventArgs Saea)
        {
            if (!Saea.AcceptSocket.SendAsync(Saea)) ProcessSend(Saea);
        }

        private void ProcessSend(SocketAsyncEventArgs Saea)
        {
            DataHolder holder = (DataHolder)Saea.UserToken;

            if (Saea.SocketError == SocketError.Success)
            {
                if (holder.OffsetSend >= this.settings.BufferSize)
                {
                    // If we are within this if-statement, then all the bytes in the message have been sent. 
                    StartReceive(Saea);
                }
                else
                {
                    // If some of the bytes in the message have NOT been sent,
                    // then we will need to post another send operation, after we store
                    // a count of how many bytes that we sent in this send op.                    
                    holder.OffsetSend += Saea.BytesTransferred;
                    // So let's loop back to StartSend().
                    StartSend(Saea);
                }
            }
            else
            {
                // We'll just close the socket if there was a
                // socket error when receiving data from the client.
                holder.Reset();
                CloseClientSocket(Saea);
            }
        }

        private void CloseClientSocket(SocketAsyncEventArgs Saea)
        {
            // do a shutdown before you close the socket
            try
            {
                Saea.AcceptSocket.Shutdown(SocketShutdown.Both);
            }
            // throws if socket was already closed
            catch (Exception)
            {
            }

            //This method closes the socket and releases all resources, both
            //managed and unmanaged. It internally calls Dispose.
            Saea.AcceptSocket.Close();

            // Put the SocketAsyncEventArg back into the pool,
            // to be used by another client. This 
            this.pool_transf_args.Push(Saea);

            // decrement the counter keeping track of the total number of clients 
            //connected to the server, for testing
            Interlocked.Decrement(ref this.accepted_num);
            this.connection_enforcer.Release();
        }

    }
}
